package com.ar4j.sql;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An implementation of the INamedQuerySource interface which loads named queries from a file and
 * is able to reload them periodically.
 * 
 */
public class ReloadableFileNamedQuerySource implements INamedQuerySource {
  private final Logger logger = LoggerFactory.getLogger(ReloadableFileNamedQuerySource.class);
  private static final String QUERY_SEPARATOR = "-- namedQuery:";
  
  private Collection<String> queryFilenames;
  private long lastReloadtimestamp;
  private long reloadInterval;
  private Map<String, String> queries;
  
  /**
   * Construct a new reloadable file query source, loading the queries from a list
   * of given filename and reloading them no demand every reloadInterval.
   */
  public ReloadableFileNamedQuerySource(String queryFilename, long reloadInterval) 
    throws IOException {
    
    this(Collections.singletonList(queryFilename), reloadInterval);
  }
  
  /**
   * Construct a new reloadable file query source, loading the queries from a list
   * of given filenames and reloading them no demand every reloadInterval.
   */
  public ReloadableFileNamedQuerySource(Collection<String> queryFilenames, long reloadInterval) 
    throws IOException {
    
    this.queryFilenames = queryFilenames;
    this.reloadInterval = reloadInterval;
    this.lastReloadtimestamp = System.currentTimeMillis();
    loadQueriesFromFiles();
  }
  
  /**
   * @see com.ar4j.sql.INamedQuerySource#findQueryByName(String)
   */
  @Override
  public String findQueryByName(String name) {
    Map<String, String> internalMap = getQueryMap();
    if(!internalMap.containsKey(name)) {
      throw new IllegalArgumentException("Query not found: " + name);
    }
    
    return internalMap.get(name);
  }
  
  /**
   * Used to retrieve the current query map. If enough time has passed since the last
   * reload of the queries, a thread is started to perform a reload operation.
   */
  private synchronized Map<String, String> getQueryMap() {
    if(System.currentTimeMillis() - lastReloadtimestamp > reloadInterval) {
      lastReloadtimestamp = System.currentTimeMillis();
      InternalFileQueryLoader loader = new InternalFileQueryLoader();
      loader.start();
    }
    
    return queries;
  }
  
  /**
   * Sets the internal query map. Used by the loader thread to
   * replace the current query map by a newly loaded one.
   */
  private synchronized void setQueryMap(Map<String, String> queries) {
    this.queries = queries;
  }
  
  /**
   * Main load method. Will open all files listed as query files and load 
   * all named queries. If duplicates are encountered an exception will be thrown.
   */
  private void loadQueriesFromFiles() 
    throws IOException {
    
    // go through all files and collect queries
    Map<String, String> queryMap = new HashMap<String, String>();
    for(String filename : queryFilenames) {
      
      // open up query file
      InputStream is = getClass().getClassLoader().getResourceAsStream(filename);
      BufferedReader reader = new BufferedReader(new InputStreamReader(is));
      
      // prepare variables and collectors
      StringBuilder currentQuery = new StringBuilder();
      String currentName = null;
      
      // go through every line in the file, record queries under their names
      String line;
      while((line = reader.readLine()) != null) {
        if(StringUtils.isBlank(line)) {
          continue;
        }
        
        // if we his a boundary, record current query and start a new record
        // otherwise just append to buffer
        if(line.startsWith(QUERY_SEPARATOR)) {
          addQueryToMap(filename, queryMap, currentName, currentQuery);
          currentQuery = new StringBuilder();
          currentName = line.substring(QUERY_SEPARATOR.length()).trim();
        } else {
          currentQuery.append(line);
          currentQuery.append("\n");
        }
      }
      
      // collect final query if there is one
      addQueryToMap(filename, queryMap, currentName, currentQuery);
     
      reader.close();
    }
    
    // record final map
    setQueryMap(Collections.synchronizedMap(queryMap));
  }
  
  /**
   * Quick utility method to add a query to a query map, checks for duplicate query names
   * and will throw an IllegalArgumentException if a duplicate is encountered.
   */
  private void addQueryToMap(String filename, Map<String, String> map, String name, StringBuilder query) {
    if(name != null) {
      String qry = query.toString();
      if(map.containsKey(name)) {
        throw new IllegalArgumentException("A duplicate query name is found in: " + filename + ", name: " + name);
      }
      map.put(name, qry);
    }
  }
  
  /**
   * Utility thread that will asynchronously trigger a query reload process.
   */
  private class InternalFileQueryLoader extends Thread {
    
    @Override
    public void run() {
      try {
        loadQueriesFromFiles();
      } catch(Throwable e) {
        logger.error("Could not load query files", e);
      }
    }
  }
  
}
